/*
 * Copyright (C) 2022 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { WireType } from '../message/WireType';
import { SessionHandShake } from '../message/SessionHandShake';
import { PayloadProtect } from '../message/PayloadProtect';
import { TransferConfig } from '../message/TransferConfig';
import { TransferPayload } from '../message/TransferPayload';
import { log } from '../../log/Log';

export class Serialize {
  static bannerByteLength: number = 8;

  static serializeSessionHandShake(handShake: SessionHandShake): Uint8Array {
    let bannerValue = this.serializeToString(1, handShake.banner);
    let authTypeValue = this.serializeU8(2, handShake.authType);
    let sessionIdValue = this.serializeU32(3, handShake.sessionId);
    let connectKey = this.serializeToString(4, handShake.connectKey);
    let buf = this.serializeToString(5, handShake.buf);
    let version = this.serializeToString(6, handShake.version);
    let mergedArray = new Uint8Array(
      bannerValue.length + authTypeValue.length + sessionIdValue.length + connectKey.length + buf.length + version.length
    );
    mergedArray.set(bannerValue);
    mergedArray.set(authTypeValue, bannerValue.length);
    mergedArray.set(sessionIdValue, bannerValue.length + authTypeValue.length);
    mergedArray.set(connectKey, bannerValue.length + authTypeValue.length + sessionIdValue.length);
    mergedArray.set(buf, bannerValue.length + authTypeValue.length + sessionIdValue.length + connectKey.length);
    mergedArray.set(version, bannerValue.length + authTypeValue.length + sessionIdValue.length + connectKey.length + buf.length);
    return mergedArray;
  }

  static serializePayloadProtect(payloadProtect: PayloadProtect): Uint8Array {
    let channelId = this.serializeU32(1, payloadProtect.channelId);
    let commandFlag = this.serializeU32(2, payloadProtect.commandFlag);
    let checkSum = this.serializeU8(3, payloadProtect.checkSum);
    let vCode = this.serializeU8(4, payloadProtect.vCode);
    let mergedArray = new Uint8Array(channelId.length + commandFlag.length + checkSum.length + vCode.length);
    mergedArray.set(channelId);
    mergedArray.set(commandFlag, channelId.length);
    mergedArray.set(checkSum, channelId.length + commandFlag.length);
    mergedArray.set(vCode, channelId.length + commandFlag.length + checkSum.length);
    return mergedArray;
  }

  static serializeTransferConfig(transferConfig: TransferConfig): Uint8Array {
    let fileSizeValue = this.serializeU64(1, transferConfig.fileSize);
    let atimeValue = this.serializeU64(2, transferConfig.atime);
    let mtimeValue = this.serializeU64(3, transferConfig.mtime);
    let optionsValue = this.serializeToString(4, transferConfig.options);
    let pathValue = this.serializeToString(5, transferConfig.path);
    let optionalNameValue = this.serializeToString(6, transferConfig.optionalName);
    let updateIfNewValue = this.serializeU32(7, transferConfig.updateIfNew ? 1 : 0);
    let compressTypeValue = this.serializeU8(8, transferConfig.compressType);
    let holdTimestampValue = this.serializeU32(9, transferConfig.holdTimestamp ? 1 : 0);
    let functionNameValue = this.serializeToString(10, transferConfig.functionName);
    let clientCwdValue = this.serializeToString(11, transferConfig.clientCwd);
    let reserve1Value = this.serializeToString(12, transferConfig.reserve1);
    let reserve2Value = this.serializeToString(13, transferConfig.reserve2);
    let mergedArray = new Uint8Array(
      fileSizeValue.length +
        atimeValue.length +
        mtimeValue.length +
        optionsValue.length +
        pathValue.length +
        optionalNameValue.length +
        updateIfNewValue.length +
        compressTypeValue.length +
        holdTimestampValue.length +
        functionNameValue.length +
        clientCwdValue.length +
        reserve1Value.length +
        reserve2Value.length
    );
    let offset = 0;
    mergedArray.set(fileSizeValue);
    offset += fileSizeValue.length;
    mergedArray.set(atimeValue, offset);
    offset += atimeValue.length;
    mergedArray.set(mtimeValue, offset);
    offset += mtimeValue.length;
    mergedArray.set(optionsValue, offset);
    offset += optionsValue.length;
    mergedArray.set(pathValue, offset);
    offset += pathValue.length;
    mergedArray.set(optionalNameValue, offset);
    offset += optionalNameValue.length;
    mergedArray.set(updateIfNewValue, offset);
    offset += updateIfNewValue.length;
    mergedArray.set(compressTypeValue, offset);
    offset += compressTypeValue.length;
    mergedArray.set(holdTimestampValue, offset);
    offset += holdTimestampValue.length;
    mergedArray.set(functionNameValue, offset);
    offset += functionNameValue.length;
    mergedArray.set(clientCwdValue, offset);
    offset += clientCwdValue.length;
    mergedArray.set(reserve1Value, offset);
    offset += reserve1Value.length;
    mergedArray.set(reserve2Value, offset);
    return mergedArray;
  }

  static serializeTransferPayload(transferPayload: TransferPayload): Uint8Array {
    let indexValue = this.serializeU64(1, transferPayload.index);
    let compressTypeValue = this.serializeU8(2, transferPayload.compressType);
    let compressSizeValue = this.serializeU32(3, transferPayload.compressSize);
    let uncompressSizeValue = this.serializeU32(4, transferPayload.uncompressSize);
    let mergedArray = new Uint8Array(
      indexValue.length + compressTypeValue.length + compressSizeValue.length + uncompressSizeValue.length
    );
    let offset = 0;
    mergedArray.set(indexValue);
    offset += indexValue.length;
    mergedArray.set(compressTypeValue, offset);
    offset += compressTypeValue.length;
    mergedArray.set(compressSizeValue, offset);
    offset += compressSizeValue.length;
    mergedArray.set(uncompressSizeValue, offset);
    return mergedArray;
  }

  static serializeToString(tag: number, value: string): Uint8Array {
    let number = this.makeTagWireType(tag, WireType.LENGTH_DELIMETED);
    let data = this.writeVarIntU32(number);
    let dataLength = this.writeVarIntU32(value.length);
    const encoder = new TextEncoder();
    const buffer = encoder.encode(value);
    let stringValue = new Uint8Array(data.length + dataLength.length + buffer.length);
    stringValue.set(data);
    stringValue.set(dataLength, data.length);
    stringValue.set(buffer, data.length + dataLength.length);
    return stringValue;
  }

  static serializeU8(tag: number, value: number): Uint8Array {
    return this.serializeU32(tag, value);
  }

  static serializeU32(tag: number, value: number): Uint8Array {
    let makeTagWireType = this.writeTagWriteType(tag, WireType.VARINT);
    let writeVarInt = this.writeVarIntU32(value);
    let mergedArray = new Uint8Array(makeTagWireType.length + writeVarInt.length);
    mergedArray.set(makeTagWireType);
    mergedArray.set(writeVarInt, makeTagWireType.length);
    return mergedArray;
  }

  static serializeU64(tag: number, value: number): Uint8Array {
    let makeTagWireType = this.writeTagWriteType(tag, WireType.VARINT);
    let writeVarInt = this.writeVarIntU64(value);
    let mergedArray = new Uint8Array(makeTagWireType.length + writeVarInt.length);
    mergedArray.set(makeTagWireType);
    mergedArray.set(writeVarInt, makeTagWireType.length);
    return mergedArray;
  }

  static writeTagWriteType(tag: number, wireType: number): Uint8Array {
    let number = this.makeTagWireType(tag, wireType);
    return this.writeVarIntU32(number);
  }

  static makeTagWireType(tag: number, wireType: number): number {
    return (tag << 3) | wireType;
  }

  static writeVarIntU64(u64value: number): Uint8Array {
    let uint64buffer: number[] = [];
    for (let index = 0; index < 10; index++) {
      uint64buffer[index] = u64value & 0b01111111;
      u64value = u64value >> 7;
      if (u64value) {
        uint64buffer[index] = uint64buffer[index] | 0b10000000;
      } else {
        break;
      }
    }
    return new Uint8Array(uint64buffer);
  }

  static writeVarIntU32(u32value: number): Uint8Array {
    let u32buffer: number[] = [];
    for (let index = 0; index < 5; index++) {
      u32buffer[index] = u32value & 0b01111111;
      u32value = u32value >> 7;
      if (u32value) {
        u32buffer[index] = u32buffer[index] | 0b10000000;
      } else {
        break;
      }
    }
    return new Uint8Array(u32buffer);
  }

  static parseHandshake(data: Uint8Array): SessionHandShake {
    // banner
    let bannerBuffer = data.buffer;
    let bannerTag = this.getTag(WireType.LENGTH_DELIMETED, new DataView(bannerBuffer));
    let bannerLengthBuf = bannerBuffer.slice(this.writeVarIntU32(this.makeTagWireType(bannerTag, WireType.LENGTH_DELIMETED)).length);
    let bannerSize = this.readVarIntU32(new DataView(bannerLengthBuf));
    let bannerDataBuffer = bannerLengthBuf.slice(this.writeVarIntU32(bannerSize).length);
    // @ts-ignore
    let banner = this.parseString(bannerDataBuffer, bannerSize);

    // authType
    let authBuffer = bannerDataBuffer.slice(bannerSize);
    let authTypeTag = this.getTag(WireType.VARINT, new DataView(authBuffer));
    let authDataBuffer = authBuffer.slice(this.writeVarIntU32(this.makeTagWireType(authTypeTag, WireType.VARINT)).length);
    let authType = this.parseU8(new DataView(authDataBuffer), 1);
    let authTypeLenth = this.writeVarIntU32(authType).length;

    // sessionId
    let sessionIdBuffer = authDataBuffer.slice(authTypeLenth);
    let sessionIdTag = this.getTag(WireType.VARINT, new DataView(sessionIdBuffer));
    let sessionDataBuffer = sessionIdBuffer.slice(this.writeVarIntU32(this.makeTagWireType(sessionIdTag, WireType.VARINT)).length);
    let sessionId = this.parseU32(new DataView(sessionDataBuffer), 3);
    let sessionIdLenth = this.writeVarIntU32(sessionId).length;

    // connectKey
    let connectKeyBuffer = sessionDataBuffer.slice(sessionIdLenth);
    let connectKeyTag = this.getTag(WireType.LENGTH_DELIMETED, new DataView(connectKeyBuffer));
    let connectLengthBuffer = connectKeyBuffer.slice(this.writeVarIntU32(this.makeTagWireType(connectKeyTag, WireType.LENGTH_DELIMETED)).length);
    let connectKeySize = this.readVarIntU32(new DataView(connectLengthBuffer));
    let connectDataBuffer = connectLengthBuffer.slice(this.writeVarIntU32(connectKeySize).length);
    // @ts-ignore
    let connectKey = this.parseString(connectDataBuffer, connectKeySize);

    // buf
    let bufBuffer = connectDataBuffer.slice(connectKeySize);
    let bufTag = this.getTag(WireType.LENGTH_DELIMETED, new DataView(bufBuffer));
    let lengthBuffer = bufBuffer.slice(this.writeVarIntU32(this.makeTagWireType(bufTag, WireType.LENGTH_DELIMETED)).length);
    let bufSize = this.readVarIntU32(new DataView(lengthBuffer));
    let dataBuffer = lengthBuffer.slice(this.writeVarIntU32(bufSize).length);
    // @ts-ignore
    let buf = this.parseString(dataBuffer, bufSize);

    // version
    let versionBuffer = dataBuffer.slice(bufSize);
    let versionTag = this.getTag(WireType.LENGTH_DELIMETED, new DataView(versionBuffer));
    let versionLengthBuffer = versionBuffer.slice(this.writeVarIntU32(this.makeTagWireType(versionTag, WireType.LENGTH_DELIMETED)).length);
    let versionSize = this.readVarIntU32(new DataView(versionLengthBuffer));
    let versionDataBuffer = versionLengthBuffer.slice(this.writeVarIntU32(versionSize).length);
    // @ts-ignore
    let version = this.parseString(versionDataBuffer, versionSize);
    return new SessionHandShake(banner, authType, sessionId, connectKey, buf, version);
  }

  static parseTransferConfig(data: ArrayBuffer): TransferConfig {
    let uint8Array = new Uint8Array(data);
    // channelId
    let dataBuffer = uint8Array.buffer;
    let channelTag = this.getTag(WireType.VARINT, new DataView(dataBuffer));
    let channelDataBuffer = dataBuffer.slice(1);
    let channelDataView = new DataView(channelDataBuffer);
    let fileSize = this.parseU64(channelDataView, 1);
    return new TransferConfig(fileSize, 0, 0, '', '', '', false, 0, false, '', '', '', '');
  }

  static parsePayloadProtect(data: ArrayBuffer): PayloadProtect {
    let uint8Array = new Uint8Array(data);

    // channelId
    let dataBuffer = uint8Array.buffer;
    let channelTag = this.getTag(WireType.VARINT, new DataView(dataBuffer));
    let channelDataBuffer = dataBuffer.slice(1);
    let channelDataView = new DataView(channelDataBuffer);
    let channelId = this.parseU32(channelDataView, 1);

    // commandFlag
    let commandTagBuffer = channelDataBuffer.slice(1);
    let commandTag = this.getTag(WireType.VARINT, new DataView(commandTagBuffer));
    let commandDataBuffer = commandTagBuffer.slice(1);
    let commandDataView = new DataView(commandDataBuffer);
    let commandFlag = this.parseU32(commandDataView, 1);

    // checkSum
    let checkSumTagBuffer = commandDataBuffer.slice(1);
    let checkSumTag = this.getTag(WireType.VARINT, new DataView(checkSumTagBuffer));
    let checkSumDataBuffer = checkSumTagBuffer.slice(1);
    let checkSumDataView = new DataView(checkSumDataBuffer);
    let checkSum = this.parseU8(checkSumDataView, 1);

    // vCode
    let vCodeTagBuffer = checkSumDataBuffer.slice(1);
    let vCodeTag = this.getTag(WireType.VARINT, new DataView(vCodeTagBuffer));
    let vCodeDataBuffer = vCodeTagBuffer.slice(1);
    let vCodeDataView = new DataView(vCodeDataBuffer);
    let vCode = this.parseU8(vCodeDataView, 1);

    return new PayloadProtect(channelId, commandFlag, checkSum, vCode);
  }

  static parseString(buffer: ArrayBuffer, size: number): string {
    let arrayBuffer = buffer.slice(0, size);
    let textDecoder = new TextDecoder();
    return textDecoder.decode(arrayBuffer);
  }

  static parseU8(dataView: DataView, size: number): number {
    return this.parseU32(dataView, size);
  }

  static parseU32(dataView: DataView, size: number): number {
    return this.readVarIntU32(dataView);
  }

  static parseU64(dataView: DataView, size: number): number {
    return this.readVarIntU64(dataView);
  }

  static readVarIntU32(dataView: DataView): number {
    let u32IntValue: number = 0;
    for (let u32IntIndex = 0; u32IntIndex < 5; u32IntIndex++) {
      if (dataView.byteLength < u32IntIndex + 1) {
        return -1;
      }
      let int8 = dataView.getUint8(u32IntIndex);
      let readValue = int8 & 0b01111111;
      u32IntValue |= readValue << (7 * u32IntIndex);
      if (!(int8 & 0b10000000)) {
        return u32IntValue;
      }
    }
    return -1;
  }

  static readVarIntU64(dataView: DataView): number {
    let u62IntValue: number = 0;
    for (let u62IntIndex = 0; u62IntIndex < 10; u62IntIndex++) {
      if (dataView.byteLength < u62IntIndex + 1) {
        return -1;
      }
      let int8 = dataView.getUint8(u62IntIndex);
      let readValue = int8 & 0b01111111;
      u62IntValue |= readValue << (7 * u62IntIndex);
      if (!(int8 & 0b10000000)) {
        return u62IntValue;
      }
    }
    return -1;
  }

  static readTagWireType(tagKey: number): number {
    let wireNewType: WireType = tagKey & 0b0111;
    return tagKey >> 3;
  }

  static getTag(wireType: WireType, dataView: DataView): number {
    let tagKey = this.readVarIntU32(dataView);
    return this.readTagWireType(tagKey);
  }
}
